-
Notifications
You must be signed in to change notification settings - Fork 10.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Implement request timeout middleware #46115
Conversation
Thanks for your PR, @Kahbazi. Someone from the team will get assigned to your PR shortly and we'll get it reviewed. |
src/Http/Http/src/Timeouts/RequestTimeoutsIEndpointConventionBuilderExtensions.cs
Outdated
Show resolved
Hide resolved
src/Http/Http/src/Timeouts/RequestTimeoutsIEndpointConventionBuilderExtensions.cs
Outdated
Show resolved
Hide resolved
src/Http/Http/src/Timeouts/RequestTimeoutsIEndpointConventionBuilderExtensions.cs
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is basically like a gRPC deadline on the server. You can see the main class for implementing that here: https://github.com/grpc/grpc-dotnet/blob/948be08b088fbf449982b731c8e1a7cf8abb1965/src/Grpc.AspNetCore.Server/Internal/ServerCallDeadlineManager.cs
gRPC implementation is a lot more complicated than needed because I wanted to avoid allocating a CancellationTokenSource unless requested. Also, it supports timeouts greater than the duration of CancellationTokenSource
.
Edit: I read the original issue and see a significant difference between this and gRPC deadlines. Unlike a deadline, this is cooperative and doesn't cancel the original request.
{ | ||
var originalToken = context.RequestAborted; | ||
var timeoutCts = new CancellationTokenSource(timespan.Value); | ||
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(context.RequestAborted, timeoutCts.Token); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Creating a linked token source is quite expensive. There are now 3 token sources for the request.
- The original RequestAborted CTS
- The timeout CTS
- The linked CTS
You can get that down to two CTS. Replace the linked CTS with subscribing to the original CTS's token, and call timeoutCts.Cancel()
in the callback. Gotchas are you have to add thread safety between timeoutCts.Cancel()
and timeoutCts.Dispose()
(canceling a disposed CTS errors), and unregister the subscription.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we need to get this down to 0 (amortized). We need to use an ObjectPool<CancellationTokenSource>
or (https://github.com/dotnet/aspnetcore/blob/4535ea1263e9a24ca8d37b7266797fe1563b8b12/src/Shared/CancellationTokenSourcePool.cs). This combined with RegisterForDispose should get the allocations per request down (removes the async state machine).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm tempted to replace this custom pool with the ObjectPool since we just improved the performance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could use IPersistentState to store the CTS on the HttpContext
. It will be cached along with the context.
That avoids any locking and contention from a shared pool.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice. We can try that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@JamesNK I'm still trying to figure out your solution. Are you suggesting to store the Original CTS from here in IPersistentState
_abortedCts = new CancellationTokenSource(); |
and then call
CancelAfter(timespan)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Tratcher Any guidance on this thread? I would like to do this one in this PR if I can.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
get the benchmarks working first, then we can measure the impact of this change
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. Could you please point me to a benchmark sample and what do I need to benchmark exactly? I'm thinking a pipeline with and without the request timeouts middleware?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- An endpoint with no metadata, no default timeout
- a default timeout
- default overridden by disable attribute
- a timeout set on an endpoint
- a named policy
- a very short timeout that fires
We didn't want a Timespan constructor for RequestTimeoutAttribute because Timespan's can't be used as attribute input. |
These two questions go together. Factor out the token creation to an internal service. That service would be responsible for both recycling tokens, as well as mocking them when needed. The mock implementation can use a manually advancing clock rather than a timer. |
81d90c5
to
dc29a51
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Revert src/submodules/googletest
We'll want a performance test for this middleware as well. We need to make it as pay for play as possible. Most requests won't be timing out, so we should optimize for that. We also need to potentially do something with long running requests cc @BrennanConroy |
How long is a long-running request? Hours or days? What I did for gRPC + long timeouts was to have a What you can do is have different paths for short timeouts (milliseconds less than 2^32) and long timeouts.
If a request is long-running then per-request allocations aren't a big concern. |
Something this middleware should ignore. I'm thinking about streaming requests:
The ability to set a global default timeout devoid of a specific endpoint breaks those scenarios. Should there be a feature to disable the timeout (similar to what we have for response buffering). |
Timeouts can already be disabled per endpoint via metadata. I understand that can be tedious/redundant, but I hesitate to try to special case all of those scenarios in the middleware. Having an opt-out feature is a bit odd because the timeout already started and may fire before it's disabled. Proposal:
|
36ae661
to
3d11e63
Compare
96ac92f
to
17dffdd
Compare
@Kahbazi I'm pushing a few cleanup updates to this PR so we can get this merged quickly. |
src/Servers/Kestrel/tools/CodeGenerator/HttpProtocolFeatureCollection.cs
Show resolved
Hide resolved
Looks like this is merged. Thank you for all your work on this @Kahbazi! |
You're welcome. Sorry I couldn't complete this on time. We are approaching new year holidays in here. |
Hi @Kahbazi. It looks like you just commented on a closed PR. The team will most probably miss it. If you'd like to bring something important up to their attention, consider filing a new issue and add enough details to build context. |
No worries at all. And happy new year! |
Implement request timeout middleware
Implement a middleware which links a cancellation token to
HttpContext.RequestAborted
in order to handle request timeout.Description
The middleware looks up the endpoint metadata and get the timespan from it. The Order is
RequestTimeoutPolicy
,RequestTimeoutAttribute
and if none of them are available, it will check theRequestTimeoutOptions.DefaultTimeout
. If theDisableRequestTimeoutAttribute
metadata is available then the middleware would be skipped.Questions:
RequestTimeoutPolicy
as a metadata. Why not just add a constructor that takesTimespan
toRequestTimeoutAttribute
and avoid looking up for both metadata?CancellationTokenSource
s be reused? Something like ActivityCancellationTokenSource.Task.Delay
in tests, but I see no other way to test this. Should I just use that?Fixes #45732